//! The evaluation part of the Read-Eval-Print-Loop (REPL)

const std = @import("std");
const base = @import("base");
const compile = @import("compile");
const parse = @import("parse");
const types = @import("types");
const can = @import("can");
const Can = can.Can;
const check = @import("check");
const Check = check.Check;
const builtins = @import("builtins");
const eval_mod = @import("eval");
const CrashContext = eval_mod.CrashContext;
const BuiltinTypes = eval_mod.BuiltinTypes;
const builtin_loading = eval_mod.builtin_loading;
const collections = @import("collections");
const reporting = @import("reporting");

const AST = parse.AST;
const Allocator = std.mem.Allocator;
const ModuleEnv = can.ModuleEnv;
const RocOps = builtins.host_abi.RocOps;
const LoadedModule = builtin_loading.LoadedModule;

/// REPL state that tracks past definitions and evaluates expressions
pub const Repl = struct {
    allocator: Allocator,
    /// Map from variable name to source string for definitions
    definitions: std.StringHashMap([]const u8),
    /// Operations for the Roc runtime
    roc_ops: *RocOps,
    /// Shared crash context managed by the host (optional)
    crash_ctx: ?*CrashContext,
    /// Optional trace writer for debugging evaluation
    //trace_writer: ?std.io.AnyWriter,
    /// ModuleEnv from last successful evaluation (for snapshot generation)
    last_module_env: ?*ModuleEnv,
    /// Debug flag to store rendered HTML for snapshot generation
    debug_store_snapshots: bool,
    /// Storage for rendered CAN HTML at each step (only when debug_store_snapshots is true)
    debug_can_html: std.array_list.Managed([]const u8),
    /// Storage for rendered TYPES HTML at each step (only when debug_store_snapshots is true)
    debug_types_html: std.array_list.Managed([]const u8),
    /// Builtin type declaration indices (loaded once at startup from builtin_indices.bin)
    builtin_indices: can.CIR.BuiltinIndices,
    /// Loaded Builtin module (loaded once at startup)
    builtin_module: LoadedModule,

    pub fn init(allocator: Allocator, roc_ops: *RocOps, crash_ctx: ?*CrashContext) !Repl {
        const compiled_builtins = @import("compiled_builtins");

        // Load builtin indices once at startup (generated at build time)
        const builtin_indices = try builtin_loading.deserializeBuiltinIndices(allocator, compiled_builtins.builtin_indices_bin);

        // Load Builtin module once at startup
        const builtin_source = compiled_builtins.builtin_source;
        var builtin_module = try builtin_loading.loadCompiledModule(allocator, compiled_builtins.builtin_bin, "Builtin", builtin_source);
        errdefer builtin_module.deinit();

        return Repl{
            .allocator = allocator,
            .definitions = std.StringHashMap([]const u8).init(allocator),
            .roc_ops = roc_ops,
            .crash_ctx = crash_ctx,
            //.trace_writer = null,
            .last_module_env = null,
            .debug_store_snapshots = false,
            .debug_can_html = std.array_list.Managed([]const u8).init(allocator),
            .debug_types_html = std.array_list.Managed([]const u8).init(allocator),
            .builtin_indices = builtin_indices,
            .builtin_module = builtin_module,
        };
    }

    // pub fn setTraceWriter(self: *Repl, trace_writer: std.io.AnyWriter) void {
    //     self.trace_writer = trace_writer;
    // }

    /// Enable debug mode to store snapshot HTML for each REPL step
    pub fn enableDebugSnapshots(self: *Repl) void {
        self.debug_store_snapshots = true;
    }

    /// Get pointer to last ModuleEnv for snapshot generation
    pub fn getLastModuleEnv(self: *Repl) ?*ModuleEnv {
        return self.last_module_env;
    }

    /// Get debug CAN HTML for all steps (only available when debug_store_snapshots is enabled)
    pub fn getDebugCanHtml(self: *Repl) []const []const u8 {
        return self.debug_can_html.items;
    }

    /// Get debug TYPES HTML for all steps (only available when debug_store_snapshots is enabled)
    pub fn getDebugTypesHtml(self: *Repl) []const []const u8 {
        return self.debug_types_html.items;
    }

    /// Allocate a new ModuleEnv and save it
    fn allocateModuleEnv(self: *Repl, source: []const u8) !*ModuleEnv {
        // Clean up previous ModuleEnv if it exists
        if (self.last_module_env) |old_env| {
            old_env.deinit();
            self.allocator.destroy(old_env);
        }

        // Allocate new ModuleEnv on heap
        const new_env = try self.allocator.create(ModuleEnv);
        var arena = std.heap.ArenaAllocator.init(self.allocator);
        defer arena.deinit();

        new_env.* = try ModuleEnv.init(self.allocator, source);
        self.last_module_env = new_env;
        return new_env;
    }

    /// Generate and store CAN and TYPES HTML for debugging
    fn generateAndStoreDebugHtml(self: *Repl, module_env: *ModuleEnv, expr_idx: can.CIR.Expr.Idx) !void {
        const SExprTree = @import("base").SExprTree;

        // Generate CAN HTML
        {
            var tree = SExprTree.init(self.allocator);
            defer tree.deinit();
            try module_env.pushToSExprTree(expr_idx, &tree);

            var can_buffer = std.ArrayList(u8).empty;
            defer can_buffer.deinit(self.allocator);
            try tree.toStringPretty(can_buffer.writer(self.allocator).any(), .include_linecol);

            const can_html = try self.allocator.dupe(u8, can_buffer.items);
            try self.debug_can_html.append(can_html);
        }

        // Generate TYPES HTML
        {
            var tree = SExprTree.init(self.allocator);
            defer tree.deinit();
            try module_env.pushTypesToSExprTree(expr_idx, &tree);

            var types_buffer = std.ArrayList(u8).empty;
            defer types_buffer.deinit(self.allocator);
            try tree.toStringPretty(types_buffer.writer(self.allocator).any(), .include_linecol);

            const types_html = try self.allocator.dupe(u8, types_buffer.items);
            try self.debug_types_html.append(types_html);
        }
    }

    /// Add or replace a definition in the REPL context
    pub fn addOrReplaceDefinition(self: *Repl, source: []const u8, var_name: []const u8) !void {
        // Check if we're replacing an existing definition
        if (self.definitions.fetchRemove(var_name)) |kv| {
            // Free both the old key and value
            self.allocator.free(kv.key);
            self.allocator.free(kv.value);
        }

        // Duplicate both key and value since they're borrowed from input
        const owned_key = try self.allocator.dupe(u8, var_name);
        const owned_source = try self.allocator.dupe(u8, source);
        try self.definitions.put(owned_key, owned_source);
    }

    pub fn deinit(self: *Repl) void {
        // Clean up definition strings and keys
        var iterator = self.definitions.iterator();
        while (iterator.next()) |kv| {
            self.allocator.free(kv.key_ptr.*); // Free the variable name
            self.allocator.free(kv.value_ptr.*); // Free the source string
        }
        self.definitions.deinit();

        // Clean up debug HTML storage
        for (self.debug_can_html.items) |html| {
            self.allocator.free(html);
        }
        self.debug_can_html.deinit();

        for (self.debug_types_html.items) |html| {
            self.allocator.free(html);
        }
        self.debug_types_html.deinit();

        // Clean up last ModuleEnv if it exists
        if (self.last_module_env) |module_env| {
            module_env.deinit();
            self.allocator.destroy(module_env);
        }

        // Clean up loaded builtin module
        self.builtin_module.deinit();
    }

    /// Process a line of input and return structured result data.
    /// This is the preferred API for programmatic use (e.g., playground, tests).
    pub fn stepStructured(self: *Repl, line: []const u8) !StepResult {
        const trimmed = std.mem.trim(u8, line, " \t\n\r");

        // Handle special commands
        if (trimmed.len == 0) {
            return .empty;
        }

        if (std.mem.eql(u8, trimmed, ":help")) {
            return .{ .help = try self.allocator.dupe(u8,
                \\Enter an expression to evaluate, or a definition (like x = 1) to use later.
                \\
                \\  - :q quits
                \\  - :help shows this text again
            ) };
        }

        if (std.mem.eql(u8, trimmed, ":exit") or
            std.mem.eql(u8, trimmed, ":quit") or
            std.mem.eql(u8, trimmed, ":q") or
            std.mem.eql(u8, trimmed, "exit") or
            std.mem.eql(u8, trimmed, "quit") or
            std.mem.eql(u8, trimmed, "exit()") or
            std.mem.eql(u8, trimmed, "quit()"))
        {
            return .quit;
        }

        // Process the input
        return try self.processInputStructured(trimmed);
    }

    /// Process a line of input and return the result as a string.
    /// This is a convenience wrapper for CLI REPL use.
    /// For programmatic use, prefer stepStructured() which returns typed data.
    pub fn step(self: *Repl, line: []const u8) ![]const u8 {
        const result = try self.stepStructured(line);
        return switch (result) {
            .expression => |s| s,
            .definition => |s| s,
            .help => |s| s,
            .parse_error => |s| s,
            .canonicalize_error => |s| s,
            .type_error => |s| s,
            .eval_error => |s| s,
            .quit => try self.allocator.dupe(u8, "Goodbye!"),
            .empty => try self.allocator.dupe(u8, ""),
        };
    }

    /// Process regular input (not special commands) - returns structured result
    fn processInputStructured(self: *Repl, input: []const u8) !StepResult {
        // Try to parse as a statement first
        const parse_result = try self.tryParseStatement(input);

        switch (parse_result) {
            .assignment => |info| {
                // Add or replace definition (duplicates the strings for ownership)
                try self.addOrReplaceDefinition(info.source, info.var_name);

                // Return descriptive output for assignments
                return .{ .definition = try std.fmt.allocPrint(self.allocator, "assigned `{s}`", .{info.var_name}) };
            },
            .import => {
                // Imports are not supported in this implementation
                return .{ .parse_error = try self.allocator.dupe(u8, "Imports not yet supported") };
            },
            .expression => {
                // Evaluate expression with all past definitions
                const full_source = try self.buildFullSource(input);
                defer self.allocator.free(full_source);

                return try self.evaluateSourceStructured(full_source);
            },
            .type_decl => {
                // Type declarations can't be evaluated
                return .empty;
            },
            .parse_error => |msg| {
                defer self.allocator.free(msg);
                return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {s}", .{msg}) };
            },
        }
    }

    /// Process regular input (not special commands) - returns string (legacy API)
    fn processInput(self: *Repl, input: []const u8) ![]const u8 {
        const result = try self.processInputStructured(input);
        return switch (result) {
            .expression => |s| s,
            .definition => |s| s,
            .help => |s| s,
            .parse_error => |s| s,
            .canonicalize_error => |s| s,
            .type_error => |s| s,
            .eval_error => |s| s,
            .quit => try self.allocator.dupe(u8, "Goodbye!"),
            .empty => try self.allocator.dupe(u8, ""),
        };
    }

    const ParseResult = union(enum) {
        assignment: struct {
            source: []const u8, // Borrowed from input
            var_name: []const u8, // Borrowed from input
        },
        import,
        expression,
        type_decl,
        parse_error: []const u8, // Must be allocator.dupe'd
    };

    /// The result of a REPL step - structured data that callers can use directly
    /// without parsing human-readable strings.
    pub const StepResult = union(enum) {
        /// Successfully evaluated an expression, contains the rendered value
        expression: []const u8,
        /// Successfully defined a variable
        definition: []const u8,
        /// Help text requested
        help: []const u8,
        /// User requested quit
        quit,
        /// Empty input
        empty,
        /// Parse error with rendered message
        parse_error: []const u8,
        /// Canonicalization error with rendered message
        canonicalize_error: []const u8,
        /// Type checking error with rendered message
        type_error: []const u8,
        /// Evaluation/runtime error with rendered message
        eval_error: []const u8,

        pub fn deinit(self: StepResult, allocator: Allocator) void {
            switch (self) {
                .expression => |s| allocator.free(s),
                .definition => |s| allocator.free(s),
                .help => |s| allocator.free(s),
                .parse_error => |s| allocator.free(s),
                .canonicalize_error => |s| allocator.free(s),
                .type_error => |s| allocator.free(s),
                .eval_error => |s| allocator.free(s),
                .quit, .empty => {},
            }
        }

        /// Returns true if this result represents an error
        pub fn isError(self: StepResult) bool {
            return switch (self) {
                .parse_error, .canonicalize_error, .type_error, .eval_error => true,
                else => false,
            };
        }

        /// Get the message/output for display, if any
        pub fn getMessage(self: StepResult) ?[]const u8 {
            return switch (self) {
                .expression => |s| s,
                .definition => |s| s,
                .help => |s| s,
                .parse_error => |s| s,
                .canonicalize_error => |s| s,
                .type_error => |s| s,
                .eval_error => |s| s,
                .quit, .empty => null,
            };
        }
    };

    /// Try to parse input as a statement
    fn tryParseStatement(self: *Repl, input: []const u8) !ParseResult {
        var arena = std.heap.ArenaAllocator.init(self.allocator);
        defer arena.deinit();

        var module_env = try ModuleEnv.init(self.allocator, input);
        defer module_env.deinit();

        // Try statement parsing
        if (parse.parseStatement(&module_env.common, self.allocator)) |ast_const| {
            var ast = ast_const;
            defer ast.deinit(self.allocator);

            if (ast.root_node_idx != 0) {
                const stmt_idx: AST.Statement.Idx = @enumFromInt(ast.root_node_idx);
                const stmt = ast.store.getStatement(stmt_idx);

                switch (stmt) {
                    .decl => |decl| {
                        const pattern = ast.store.getPattern(decl.pattern);
                        if (pattern == .ident) {
                            // Extract the identifier name from the pattern
                            const ident_tok = pattern.ident.ident_tok;
                            const token_region = ast.tokens.resolve(ident_tok);
                            const ident_name = module_env.common.source[token_region.start.offset..token_region.end.offset];

                            // Return borrowed strings (no duplication needed)
                            return ParseResult{ .assignment = .{
                                .source = input,
                                .var_name = ident_name,
                            } };
                        }
                        return ParseResult.expression;
                    },
                    .import => return ParseResult.import,
                    .type_decl => return ParseResult.type_decl,
                    else => return ParseResult.expression,
                }
            }
        } else |_| {
            // Statement parse failed, continue to try expression parsing
        }

        // Try expression parsing
        if (parse.parseExpr(&module_env.common, self.allocator)) |ast_const| {
            var ast = ast_const;
            defer ast.deinit(self.allocator);
            if (ast.root_node_idx != 0) {
                return ParseResult.expression;
            }
        } else |_| {
            // Expression parse failed too
        }

        return ParseResult{ .parse_error = try self.allocator.dupe(u8, "Failed to parse input") };
    }

    /// Build full source including all definitions wrapped in block syntax
    pub fn buildFullSource(self: *Repl, current_expr: []const u8) ![]const u8 {
        // If no definitions exist, just return the expression as-is
        if (self.definitions.count() == 0) {
            return try self.allocator.dupe(u8, current_expr);
        }

        var buffer = std.ArrayList(u8).empty;
        errdefer buffer.deinit(self.allocator);

        // Start block
        try buffer.appendSlice(self.allocator, "{\n");

        // Add all definitions in order
        var iterator = self.definitions.iterator();
        while (iterator.next()) |kv| {
            try buffer.appendSlice(self.allocator, "    ");
            try buffer.appendSlice(self.allocator, kv.value_ptr.*);
            try buffer.append(self.allocator, '\n');
        }

        // Add current expression
        try buffer.appendSlice(self.allocator, "    ");
        try buffer.appendSlice(self.allocator, current_expr);
        try buffer.append(self.allocator, '\n');

        // End block
        try buffer.append(self.allocator, '}');

        return try buffer.toOwnedSlice(self.allocator);
    }

    /// Evaluate source code - returns structured result
    fn evaluateSourceStructured(self: *Repl, source: []const u8) !StepResult {
        const module_env = try self.allocateModuleEnv(source);
        return try self.evaluatePureExpressionStructured(module_env);
    }

    /// Evaluate source code - returns string (legacy API)
    fn evaluateSource(self: *Repl, source: []const u8) ![]const u8 {
        const result = try self.evaluateSourceStructured(source);
        return switch (result) {
            .expression => |s| s,
            .definition => |s| s,
            .help => |s| s,
            .parse_error => |s| s,
            .canonicalize_error => |s| s,
            .type_error => |s| s,
            .eval_error => |s| s,
            .quit => try self.allocator.dupe(u8, "Goodbye!"),
            .empty => try self.allocator.dupe(u8, ""),
        };
    }

    /// Evaluate a program (which may contain definitions)
    fn evaluatePureExpression(self: *Repl, module_env: *ModuleEnv) ![]const u8 {

        // Determine if we have definitions (which means we built a block expression)
        const has_definitions = self.definitions.count() > 0;

        // Parse appropriately based on whether we have definitions
        var parse_ast = if (has_definitions)
            // Has definitions - we built a block expression, parse as expression
            parse.parseExpr(&module_env.common, self.allocator) catch |err| {
                return try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err});
            }
        else
            // No definitions - simple expression, parse as expression
            parse.parseExpr(&module_env.common, self.allocator) catch |err| {
                return try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err});
            };
        defer parse_ast.deinit(self.allocator);

        // Check for parse errors and render them
        if (parse_ast.hasErrors()) {
            // Render the first error as the error message
            if (parse_ast.tokenize_diagnostics.items.len > 0) {
                var report = try parse_ast.tokenizeDiagnosticToReport(
                    parse_ast.tokenize_diagnostics.items[0],
                    self.allocator,
                    null,
                );
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                return try output.toOwnedSlice();
            } else if (parse_ast.parse_diagnostics.items.len > 0) {
                var report = try parse_ast.parseDiagnosticToReport(
                    &module_env.common,
                    parse_ast.parse_diagnostics.items[0],
                    self.allocator,
                    "repl",
                );
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                // Trim trailing newlines from the output and return a properly allocated copy
                const full_result = try output.toOwnedSlice();
                defer self.allocator.free(full_result);
                const trimmed = std.mem.trimRight(u8, full_result, "\n");
                return try self.allocator.dupe(u8, trimmed);
            }
        }

        // Empty scratch space
        parse_ast.store.emptyScratch();

        // Create CIR
        const cir = module_env; // CIR is now just ModuleEnv
        try cir.initCIRFields("repl");

        // Get Bool, Try, and Str statement indices from the IMPORTED modules (not copied!)
        // These refer to the actual statements in the Builtin module
        const bool_stmt_in_bool_module = self.builtin_indices.bool_type;
        const try_stmt_in_try_module = self.builtin_indices.try_type;
        const str_stmt_in_builtin_module = self.builtin_indices.str_type;

        const module_builtin_ctx: Check.BuiltinContext = .{
            .module_name = try module_env.insertIdent(base.Ident.for_text("repl")),
            .bool_stmt = bool_stmt_in_bool_module,
            .try_stmt = try_stmt_in_try_module,
            .str_stmt = str_stmt_in_builtin_module,
            .builtin_module = self.builtin_module.env,
            .builtin_indices = self.builtin_indices,
        };

        // Create canonicalizer with nested types available for qualified name resolution
        // Populate all auto-imported builtin types using the shared helper to keep behavior consistent
        var module_envs_map = std.AutoHashMap(base.Ident.Idx, can.Can.AutoImportedType).init(self.allocator);
        defer module_envs_map.deinit();

        try Can.populateModuleEnvs(
            &module_envs_map,
            module_env,
            self.builtin_module.env,
            self.builtin_indices,
        );

        var czer = Can.init(cir, &parse_ast, &module_envs_map) catch |err| {
            return try std.fmt.allocPrint(self.allocator, "Canonicalize init error: {}", .{err});
        };
        defer czer.deinit();

        // NOTE: True/False/Ok/Err are now just anonymous tags that unify with Bool/Try automatically!
        // No need to register unqualified_nominal_tags - the type system handles it.

        // Since we're always parsing as expressions now, handle them the same way
        const expr_idx: AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx);

        const canonical_expr = try czer.canonicalizeExpr(expr_idx) orelse {
            // Check for diagnostics and render them as error messages
            const diagnostics = try module_env.getDiagnostics();
            if (diagnostics.len > 0) {
                // Render the first diagnostic as the error message
                const diagnostic = diagnostics[0];
                var report = try module_env.diagnosticToReport(diagnostic, self.allocator, "repl");
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                return try output.toOwnedSlice();
            }
            return try self.allocator.dupe(u8, "Canonicalize expr error: expression returned null");
        };
        const final_expr_idx = canonical_expr.get_idx();

        // Type check - Pass Builtin as imported module
        const imported_modules = [_]*const ModuleEnv{self.builtin_module.env};

        // Resolve imports - map each import to its index in imported_modules
        // This uses the same resolution logic as compile_package.zig
        module_env.imports.resolveImports(module_env, &imported_modules);

        var checker = Check.init(
            self.allocator,
            &module_env.types,
            cir,
            &imported_modules,
            &module_envs_map,
            &cir.store.regions,
            module_builtin_ctx,
        ) catch |err| {
            return try std.fmt.allocPrint(self.allocator, "Type check init error: {}", .{err});
        };
        defer checker.deinit();

        // Check the expression (no need to check defs since we're parsing as expressions)
        _ = checker.checkExprRepl(final_expr_idx) catch |err| {
            return try std.fmt.allocPrint(self.allocator, "Type check expr error: {}", .{err});
        };

        // Create interpreter instance with BuiltinTypes containing real Builtin module
        const builtin_types_for_eval = BuiltinTypes.init(self.builtin_indices, self.builtin_module.env, self.builtin_module.env, self.builtin_module.env);
        var interpreter = eval_mod.Interpreter.init(self.allocator, module_env, builtin_types_for_eval, self.builtin_module.env, &imported_modules, &checker.import_mapping, null) catch |err| {
            return try std.fmt.allocPrint(self.allocator, "Interpreter init error: {}", .{err});
        };
        defer interpreter.deinitAndFreeOtherEnvs();

        if (self.crash_ctx) |ctx| {
            ctx.reset();
        }

        const result = interpreter.eval(final_expr_idx, self.roc_ops) catch |err| switch (err) {
            error.Crash => {
                if (self.crash_ctx) |ctx| {
                    if (ctx.crashMessage()) |msg| {
                        return try std.fmt.allocPrint(self.allocator, "Crash: {s}", .{msg});
                    }
                }
                return try self.allocator.dupe(u8, "Evaluation error: error.Crash");
            },
            else => return try std.fmt.allocPrint(self.allocator, "Evaluation error: {}", .{err}),
        };

        // Generate debug HTML if enabled
        if (self.debug_store_snapshots) {
            try self.generateAndStoreDebugHtml(module_env, final_expr_idx);
        }

        // Use the runtime type from the result value if available (set by e.g. makeBoolValue),
        // otherwise fall back to translating the compile-time type from the expression.
        // This is important when the compile-time type is a generic constraint (e.g. from == or !=)
        // but the runtime type is concrete (e.g. Bool).
        const output = blk: {
            if (result.rt_var) |rt_var| {
                break :blk try interpreter.renderValueRocWithType(result, rt_var, self.roc_ops);
            }
            const expr_ct_var = can.ModuleEnv.varFrom(final_expr_idx);
            const expr_rt_var = interpreter.translateTypeVar(module_env, expr_ct_var) catch {
                break :blk try interpreter.renderValueRoc(result);
            };
            break :blk try interpreter.renderValueRocWithType(result, expr_rt_var, self.roc_ops);
        };

        result.decref(&interpreter.runtime_layout_store, self.roc_ops);
        return output;
    }

    /// Evaluate a program (which may contain definitions) - returns structured result
    fn evaluatePureExpressionStructured(self: *Repl, module_env: *ModuleEnv) !StepResult {
        // Determine if we have definitions (which means we built a block expression)
        const has_definitions = self.definitions.count() > 0;

        // Parse appropriately based on whether we have definitions
        var parse_ast = if (has_definitions)
            parse.parseExpr(&module_env.common, self.allocator) catch |err| {
                return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err}) };
            }
        else
            parse.parseExpr(&module_env.common, self.allocator) catch |err| {
                return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err}) };
            };
        defer parse_ast.deinit(self.allocator);

        // Check for parse errors and render them
        if (parse_ast.hasErrors()) {
            if (parse_ast.tokenize_diagnostics.items.len > 0) {
                var report = try parse_ast.tokenizeDiagnosticToReport(
                    parse_ast.tokenize_diagnostics.items[0],
                    self.allocator,
                    null,
                );
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                return .{ .parse_error = try output.toOwnedSlice() };
            } else if (parse_ast.parse_diagnostics.items.len > 0) {
                var report = try parse_ast.parseDiagnosticToReport(
                    &module_env.common,
                    parse_ast.parse_diagnostics.items[0],
                    self.allocator,
                    "repl",
                );
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                const full_result = try output.toOwnedSlice();
                defer self.allocator.free(full_result);
                const trimmed = std.mem.trimRight(u8, full_result, "\n");
                return .{ .parse_error = try self.allocator.dupe(u8, trimmed) };
            }
        }

        // Empty scratch space
        parse_ast.store.emptyScratch();

        // Create CIR
        const cir = module_env;
        try cir.initCIRFields("repl");

        // Populate all auto-imported builtin types using the shared helper to keep behavior consistent
        var module_envs_map = std.AutoHashMap(base.Ident.Idx, can.Can.AutoImportedType).init(self.allocator);
        defer module_envs_map.deinit();

        try Can.populateModuleEnvs(
            &module_envs_map,
            module_env,
            self.builtin_module.env,
            self.builtin_indices,
        );

        var czer = Can.init(cir, &parse_ast, &module_envs_map) catch |err| {
            return .{ .canonicalize_error = try std.fmt.allocPrint(self.allocator, "Canonicalize init error: {}", .{err}) };
        };
        defer czer.deinit();

        const expr_idx: AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx);

        const canonical_expr = try czer.canonicalizeExpr(expr_idx) orelse {
            const diagnostics = try module_env.getDiagnostics();
            if (diagnostics.len > 0) {
                const diagnostic = diagnostics[0];
                var report = try module_env.diagnosticToReport(diagnostic, self.allocator, "repl");
                defer report.deinit();

                var output = std.array_list.Managed(u8).init(self.allocator);
                var unmanaged = output.moveToUnmanaged();
                var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged);
                report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) {
                    error.WriteFailed => return error.OutOfMemory,
                    else => return err,
                };
                unmanaged = writer_alloc.toArrayList();
                output = unmanaged.toManaged(self.allocator);
                return .{ .canonicalize_error = try output.toOwnedSlice() };
            }
            return .{ .canonicalize_error = try self.allocator.dupe(u8, "Canonicalize expr error: expression returned null") };
        };
        const final_expr_idx = canonical_expr.get_idx();

        const imported_modules = [_]*const ModuleEnv{self.builtin_module.env};

        // Resolve imports - map each import to its index in imported_modules
        module_env.imports.resolveImports(module_env, &imported_modules);

        const builtin_ctx: Check.BuiltinContext = .{
            .module_name = try module_env.insertIdent(base.Ident.for_text("repl")),
            .bool_stmt = self.builtin_indices.bool_type,
            .try_stmt = self.builtin_indices.try_type,
            .str_stmt = self.builtin_indices.str_type,
            .builtin_module = self.builtin_module.env,
            .builtin_indices = self.builtin_indices,
        };

        var checker = Check.init(
            self.allocator,
            &module_env.types,
            cir,
            &imported_modules,
            &module_envs_map,
            &cir.store.regions,
            builtin_ctx,
        ) catch |err| {
            return .{ .type_error = try std.fmt.allocPrint(self.allocator, "Type check init error: {}", .{err}) };
        };
        defer checker.deinit();

        _ = checker.checkExprRepl(final_expr_idx) catch |err| {
            return .{ .type_error = try std.fmt.allocPrint(self.allocator, "Type check expr error: {}", .{err}) };
        };

        // Check for type problems (e.g., type mismatches)
        if (checker.problems.problems.items.len > 0) {
            // Return a generic type error message
            // TODO: Use ReportBuilder to produce a more detailed error message
            return .{ .type_error = try self.allocator.dupe(u8, "TYPE MISMATCH") };
        }

        const builtin_types_for_eval = BuiltinTypes.init(self.builtin_indices, self.builtin_module.env, self.builtin_module.env, self.builtin_module.env);
        var interpreter = eval_mod.Interpreter.init(self.allocator, module_env, builtin_types_for_eval, self.builtin_module.env, &imported_modules, &checker.import_mapping, null) catch |err| {
            return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Interpreter init error: {}", .{err}) };
        };
        defer interpreter.deinitAndFreeOtherEnvs();

        if (self.crash_ctx) |ctx| {
            ctx.reset();
        }

        const result = interpreter.eval(final_expr_idx, self.roc_ops) catch |err| switch (err) {
            error.Crash => {
                if (self.crash_ctx) |ctx| {
                    if (ctx.crashMessage()) |msg| {
                        return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Crash: {s}", .{msg}) };
                    }
                }
                return .{ .eval_error = try self.allocator.dupe(u8, "Evaluation error: error.Crash") };
            },
            else => return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Evaluation error: {}", .{err}) },
        };

        if (self.debug_store_snapshots) {
            try self.generateAndStoreDebugHtml(module_env, final_expr_idx);
        }

        const output = try interpreter.renderValueRocWithType(result, result.rt_var, self.roc_ops);

        result.decref(&interpreter.runtime_layout_store, self.roc_ops);
        interpreter.cleanupBindings(self.roc_ops);
        return .{ .expression = output };
    }
};
